Java
执行命令的方法ProcessBuilder
- 反射中使用
getConstructor
获取构造有参构造函数- 可变长参数(
varargs
)在反射中的意义与使用getDeclared
系列反射函数和普通反射的区别于使用
并解决第二篇的两个问题,
- 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
- 如果一个方法或构造方法是私有方法,我们是否能执行它呢?
Java安全[反射(3)]
getConstructor反射方法/ProcessBulider执行命令
第一个问题
如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
我们需要用到一个新的反射方法getConstructor
。
与getMethod
类似,getConstructor
接收参数是构造函数的列表类型。因为构造函数也支持重载,所以可能会存在多个构造函数,所以必须用参数列表类型才能唯一确认一个构造函数。
获取了构造函数后,使用newInstance
来执行。
比如,我们常用的另一个执行命令的方式ProcessBulider
,
下面是一个简单的
ProcessBuilder
使用流程:
创建一个
ProcessBuilder
实例:
1 ProcessBuilder pb = new ProcessBuilder();设置命令和参数:
1 pb.command("myCommand", "myArg1", "myArg2");(可选)设置其他属性,如工作目录、环境变量等。
启动进程:
1 Process process = pb.start();等待进程完成并获取退出值:
1 int exitValue = process.waitFor();
我们使用getConstructor
来获取其构造函数,然后调用start()
来执行命令。
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
这里的ProcessBuilder
类的构造函数有两个,而且都是有参数的
public ProcessBuilder(String... command)
public ProcessBuilder(List<String> command)
这里用到的是第二个构造函数,也可以看到构造函数的参数就是command
,也就是执行的命令需要在实例化的时候传入。
List.class
和之前 前两篇提到的String.class
一样,指的是调用方法的参数类型
。
List.class
指的就算List接口类
的Class
对象
String.class
表示String
类的Class
对象
在这段代码中,
List.class
被用作参数传递给getConstructor()
方法,以获取一个接受List
类型参数的构造函数。这意味着我们正在查找一个构造函数,它接受一个List
对象作为参数,并使用该List
对象来初始化新创建的ProcessBuilder
实例。于是就找到了第二个构造函数,这样,我们就可以动态地创建并启动一个新进程。
避免利用强类型转换
但是我们在payload
中用到了Java中的强类型转换【((ProcessBuilder) xxx)
】,有时候我们利用漏洞的时候(在表达式上下文)是没有这种语法的。所以我们仍然需要反射来完成这一步。
其中有个
Arrays.asList
其实也好理解,就算将参数从数组转换为列表,使其符合构造函数的参数类型,然后newInstance
时将参数传进去执行。
1 | ((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start(); |
其实也很好改这个payload
,这里需要用到强类型转化的原因主要是因为执行.start()
方法启动进程
的时候,前部分不用强类型包含起来,无法找到这个方法,会在Object
类中寻找。
这里可以直接用反射中getMethod
方法获取start
方法,就可以避免这种问题,然后invoke
执行,因为start
的是一个普通方法,所以invoke
的第一个参数就是ProcessBuilder
类实例。
1 | Class<?> clazz = Class.forName("java.lang.ProcessBuilder"); |
执行结果,
可变长参数(varargs)在反射中的意义与使用
那么,如果想要用ProcessBuilder
的第一个构造函数,又应该怎么实现反射呢?
1 | public ProcessBuilder(String... command) |
这里涉及到Java的变长参数了,和其他语言一样,Java中也支持可变长参数,就算当你在定义函数时,在设置参数时,不确定参数的个数时,可以用 ...
来表示这个函数的参数个数是可以变的。其实就是和数组差不多的含义。
而且实际上,对于变长参数
,Java在编译的时候会编译成一个一维数组
,也就是说,对于如下两段代码在底层上是一致的,也就是说无法重载,见下图可知。
1 | public void hello(String[] names) {} |
也就是说,如果有个数组,想传给hello
函数,直接传入数组即可
1 | String[] names = {"hello", "world"}; |
所以,我们将字符串数组的类String[].class
传给getConstructor
,就可以查找获取ProcessBuilder
的第二种构造函数:
1 | class clazz = Class.forName("java.lang.ProcessBulider"); |
但是在通过newInstance
传参时,就有不同了,因为ProcessBulider
的第一个构造函数的参数是变长参数,也就是一维数组,而newInstance
的参数也是变长参数,同样也是一维数组,如下图,所以想通过如果想传参成功,就是一个一维数组中元素为一维数组 ==> 也就是二维数组。
于是构造payload如下,
1 | Class<?> clazz = Class.forName("java.lang.ProcessBuilder"); |
这样的话,我们想要传给构造函数的参数,也就是一维数组,就被当作传给newInstance
的二维数组的元素形式,传给了构造函数。
这里可能会产生一个疑惑,不是newInstance
也是接收一个一维数组吗,为什么这里可以是二维数组,但是实际上这里的二维数组起的作用也只是一个一维数组,因为它的元素只能有一个一维数组。
如下,将二维数组中加入两个一维数组元素后,发生报错,期待的参数只有一个,但是却传入了两个,说明这里本质还是需要一个一维数组
。
那如果只在newIntance
中传入一个一维数组呢?可以看到如果直接将一个一维数组当作参数传入,newInstance
就会当作传入的三个元素【"1","2","3"
】都是一个数组,也就是当作传入了三个数组,而没有把整个数组当作一个对象发送给构造函数中去。
而newInstance
需要的是一个对象类型的变长参数,所以只需要强类型转换将我们传入的数组整体当作一个对象类型就行。
可以看到成功运行,不过一般情况是用不了强类型转换的,只能用反射之类的方法。
那为什么传入二维数组的时候不用强类型转换呢?
虽然传入的是二维数组,但实际真正的对象是其元素,也就是一维数组,所以如果直接强类型把二维数组也当做对象传给newIntance
反而会报错,newIntance
参数类型不匹配,因为newIntrance
期待的也是一个一维数组,也就是起作用的只是二维数组中那一个一维数组。
根据p神建议将payload
改成全反射方法,
1 | Class<?> clazz = Class.forName("java.lang.ProcessBuilder"); |
也是可以的,
getDeclared
这里就是解决第二个问题
如果一个方法或构造方法是私有方法,我们是否能执行它呢?
这里就引入了一个getDeclared
系列的反射,和getMethod
、getConstructor
区别在于
getMethod
系列方法获取的是当前类中所有的公共方法,包括从父类继承的方法getDeclared
系列方法获取的是当前类中声明
的方法,包括私有方法,但是是必须写在类中的,如果是从父类继承而来的就不包含了。
其中getDeclaredMethod
和getDeclaredConstructor
的具体用法,与getMethod
和getConstructor
类似,区别如上所述。
在此第二篇讲过,Runtime
的构造函数是私有的,是通过静态方法Runtime.getRuntime()
获取其运行实例现在了解了getDeclaredConstructor
,就可以通过这个获取Runtime
的私有的构造方法来实例化对象,进而执行命令。
1 | Class clazz = Class.forName("java.lang.Runtime"); |
这里就是将
1 | xx.invoke(clazz.getMethod("getRuntime").invoke(null), "calc"); |
替换为
1 | xx.invoke(clazz.getDeclaredConstructor().newInstance(), "calc"); |
setAccessible
运行发生报错,
这里报错在p神的文章中说到,这里必须要使用一个方法
setAccessible
,在获取到了一个私有方法后,必须用setAccessible
修改器作用域,否则仍然不能调用。
所有在这里就报错提醒Runtime
构造函数是私有的无法获取,只需要设置setAccessible
为true
即可拥有访问域。
1 | public class Runtime_Getdeclared { |
但是还是报错,
问了AI才知道,
这个错误是因为Java 9引入的模块系统。在模块化Java应用程序中,一个模块只能访问到它明确打开给其他模块的包。在你的情况下,
java.lang
包没有被打开给你的模块,所以你不能访问它的私有成员从
Java 9
开始,setAccessible(true)
不再总是能成功地使得私有成员可访问。如果一个包没有被打开给你的模块,那么尝试访问它的私有成员将会抛出InaccessibleObjectException
正好有个Java8
,试试改一下编译器环境变量再跑一下
可能会报错,这是因为这个项目我们已经用高版本的JDK
编译过一次了,而高版本能兼容低版本的,但是低版本就无法运行高版本的,所以会报错
于是直接写个txt
跑Java8
发现运行成功,当然虽然是低版本,但是setAccessible
还是必须存在的,